package cn.tedu.mall.seckill.timer.job;

import cn.tedu.mall.common.config.PrefixConfiguration;
import cn.tedu.mall.pojo.seckill.model.SeckillSku;
import cn.tedu.mall.pojo.seckill.model.SeckillSpu;
import cn.tedu.mall.seckill.mapper.SeckillSkuMapper;
import cn.tedu.mall.seckill.mapper.SeckillSpuMapper;
import cn.tedu.mall.seckill.utils.SeckillCacheUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.math.RandomUtils;
import org.quartz.Job;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;

import java.time.LocalDateTime;
import java.util.List;
import java.util.concurrent.TimeUnit;

@Slf4j
public class SeckillInitialJob implements Job {

    // 装配查询sku库存数相关的mapper
    @Autowired
    private SeckillSkuMapper seckillSkuMapper;
    // 装配查询spu库存数相关的mapper
    @Autowired
    private SeckillSpuMapper seckillSpuMapper;
    @Autowired
    private RedisTemplate redisTemplate;

    /*
    RedisTemplate对象在保存数据时,会讲数据进行序列化然后保存
    这样做的好处是有效利用空间的同时,也能保存较好的读写效率
    但是它的缺点是不能直接向Redis发送修改数据内容的指令
    现在我们需要保存的是sku的库存数,在高并发的情况下,如果我们取出库存数值进行修改
    就会引起线程安全问题,会发生"超卖"现象
    如果要想防止"超卖"现象,就要更换我们操作Redis的对象
    StringRedisTemplate是Spring Data Redis框架提供的另一个能够操作Redis的对象
    因为这个对象在保存数据时,只支持保存String格式,所以可以直接在Redis内部修改值
    库存就可以在Redis内部直接进行减少,即使有很多的线程同时访问,也不会有线程安全问题
    最后也要知道Redis内部操作数据的线程只有一条(Redis天生单线程),这样就能防止超卖了
    如果我们的Redis是集群模式的,要保证集群数据的同步,就需要使用redission分布式锁
     */
    // 装配能够直接在Redis内部操作数据的对象,防止超卖
    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @Override
    public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
        // 目标1:将sku的库存数预热到redis
        // 所谓预热一定是在秒杀开始前,时间自定,我们规定提前5分钟进行预热
        // 定义一个时间,是5分钟后的时间,也就是秒杀已经开始的时间
        LocalDateTime time=LocalDateTime.now().plusMinutes(5);
        // 用这个时间去查询正在进行秒杀的商品
        List<SeckillSpu> seckillSpus=
                seckillSpuMapper.findSeckillSpusByTime(time);
        // 遍历seckillSpus,也就是当前批次所有秒杀商品spu信息
        for(SeckillSpu spu: seckillSpus){
            // 库存是保存在sku中的,所以要根据当前spu的spuId,查询sku列表
            List<SeckillSku> seckillSkus=
                    seckillSkuMapper.findSeckillSkusBySpuId(spu.getSpuId());
            // 遍历seckillSkus集合,从集合的元素中获取库存数,以保存到Redis
            for(SeckillSku sku: seckillSkus){
                log.info("开始将{}号sku库存数预热到Redis",sku.getSkuId());
                // PrefixConfiguration.SeckillPrefixConfiguration.SECKILL_SKU_STOCK_PREFIX
                // 要操作redis,首先要确定key值,而key值必须是常量状态的
                // 程序中定义了秒杀过程中redis key需要常量,可以使用SeckillCacheUtils调用
                // 方法参数就是对应的id值,会自动追加到字符串中,最后的常量值可能为:
                // skuStockKey   ->   "mall:seckill:sku:stock:1"
                String skuStockKey=
                        SeckillCacheUtils.getStockKey(sku.getSkuId());
                // 判断当前Redis是否已经有这个key
                if(redisTemplate.hasKey(skuStockKey)){
                    // 如果当前Redis已经存在这个key,证明之前缓存过了,输出到日志提示即可
                    log.info("{}号sku的库存数,已经缓存过了!",sku.getSkuId());
                }else{
                    // 如果当前Redis没有这个key,就要执行将库存数预热到Redis
                    stringRedisTemplate.boundValueOps(skuStockKey).set(
                            sku.getSeckillStock()+"",
                            // 实际开发保存时间:秒杀持续时间+提前预热5分钟+防雪崩随机数30秒
                            // 1000*60*60*2+1000*60*5+ RandomUtils.nextInt(1000*30),
                            // 测试学习过程中,保存5分钟,加10秒随机数
                            1000*60*5+RandomUtils.nextInt(1000*10),
                            TimeUnit.MILLISECONDS);
                    log.info("{}号sku的库存数,成功预热到Redis",sku.getSkuId());
                }
            }
            // 上面的sku循环(内层循环)结束了,库存的预热就结束了
            // 下面开始目标2
            // 目标2:将spu对应的随机码预热到redis
            // 随机码其实就是我们自定义的一个随机数,它的目的是防止黄牛抢购
            // 操作随机码也是先获取到key
            // randCodeKey   ->   mall:seckill:spu:url:rand:code:2
            String randCodeKey=SeckillCacheUtils
                            .getRandCodeKey(spu.getSpuId());
            // 判断这个key是否已经在Redis中
            if(redisTemplate.hasKey(randCodeKey)){
                // 如果存在,直接跳过即可
                // 但是学习过程中,我们需要随机码才能提交订单,所以将随机码输出到控制台
                int randCode= (int) redisTemplate.boundValueOps(randCodeKey).get();
                log.info("{}号spu商品的随机码已经缓存了,值为:{}",spu.getSpuId(),randCode);
            }else{
                // 如果不存在,就要生成随机码,保存到Redis中
                // 下面的代码能生成一个100000~999999的随机码
                int randCode=RandomUtils.nextInt(900000)+100000;
                redisTemplate.boundValueOps(randCodeKey).set(
                        randCode,
                        // 这里也是为了方法学习和测试,保存5分钟
                        1000*60*5+RandomUtils.nextInt(10000),
                        TimeUnit.MILLISECONDS);
                log.info("{}号spu商品的随机码预热成功!值为:{}",spu.getSpuId(),randCode);
            }
        }
    }
}
